﻿#region License, Terms and Conditions
//
// Jayrock - A JSON-RPC implementation for the Microsoft .NET Framework
// Written by Atif Aziz (www.raboof.com)
// Copyright (c) Atif Aziz. All rights reserved.
//
// This library is free software; you can redistribute it and/or modify it under
// the terms of the GNU Lesser General Public License as published by the Free
// Software Foundation; either version 3 of the License, or (at your option)
// any later version.
//
// This library is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
// details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with this library; if not, write to the Free Software Foundation, Inc.,
// 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
//
#endregion

#if !NET_1_0 && !NET_1_1 && !NET_2_0

namespace Jayrock.GeoJson
{
    #region Imports

    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Collections.ObjectModel;
    using System.Diagnostics;
    using System.Runtime.Serialization;
    using System.Text;
    using Jayrock.Json;
    using Jayrock.Json.Conversion;

    #endregion

    [Serializable]
    public sealed class GeoJsonObject
    {
        private readonly GeoJsonObjectType _objectType;
        private JsonObject _members;
        private GeoJsonObjectCollection _items;
        private object _coordinates;
        private JsonObject _properties;
        private GeoJsonObject _geometry;
        private object _id;

        sealed class GeoJsonObjectCollection : Collection<GeoJsonObject>
        {
            private readonly GeoJsonObject _owner;
            private readonly Predicate<GeoJsonObject> _validator;

            public GeoJsonObjectCollection(GeoJsonObject owner, Predicate<GeoJsonObject> validator)
            {
                _owner = owner;
                _validator = validator;
            }

            protected override void InsertItem(int index, GeoJsonObject item)
            {
                ValidateItem(item);
                base.InsertItem(index, item);
            }

            protected override void SetItem(int index, GeoJsonObject item)
            {
                ValidateItem(item);
                base.SetItem(index, item);
            }

            private void ValidateItem(GeoJsonObject item)
            {
                if (!_validator(item))
                {
                    var message = String.Format(
                        @"GeoJSON {0} object is an invalid collection item for a GeoJSON {1} object.", 
                        item.ObjectType, _owner.ObjectType);
                    throw new ArgumentException(message, "item");
                }
            }
        }

        public GeoJsonObject(GeoJsonObjectType objectType)
        {
            _objectType = objectType;
        }

        public GeoJsonObjectType ObjectType { get { return _objectType; } }

        public bool IsCollection
        {
            get
            {
                GeoJsonObjectType type = ObjectType;
                return type == GeoJsonObjectType.FeatureCollection
                    || type == GeoJsonObjectType.GeometryCollection;
            }
        }

        public bool IsGeometry
        {
            get
            {
                GeoJsonObjectType type = ObjectType;
                return type != GeoJsonObjectType.Feature
                    && type != GeoJsonObjectType.FeatureCollection;
            }
        }

        public bool SupportsCoordinates
        {
            get
            {
                return IsGeometry 
                    && ObjectType != GeoJsonObjectType.GeometryCollection;
            }
        }

        public object Coordinates
        {
            get { CoordinatesOpGuard(); return _coordinates; }
            set { CoordinatesOpGuard(); _coordinates = value; }
        }

        private void CoordinatesOpGuard()
        {
            if (!SupportsCoordinates)
                throw new InvalidOperationException();
        }

        public GeoJsonObject FeatureGeometry
        {
            get { FeatureOpGuard(); return _geometry; }
            set
            {
                FeatureOpGuard(); 
                if (value != null && !value.IsGeometry)
                    throw new ArgumentException("Invalid GeoJSON object for GeoJSON Feature geometry.", "value");
                _geometry = value;
            }
        }

        public object FeatureId
        {
            get { FeatureOpGuard(); return _id; }
            set { FeatureOpGuard(); _id = value; }
        }

        private void FeatureOpGuard()
        {
            if (ObjectType != GeoJsonObjectType.Feature)
                throw new InvalidOperationException();
        }

        public bool HasMembers { get { return _members != null && _members.Count > 0; } }

        public JsonObject Members
        {
            get { return (_members ?? (_members = new JsonObject())); }
            internal set { _members = value; }
        }

        public bool HasItems { get { return _items != null && _items.Count > 0; } }

        public ICollection<GeoJsonObject> Items
        {
            get
            {
                var type = ObjectType;
                if (!(type == GeoJsonObjectType.FeatureCollection
                      || type == GeoJsonObjectType.GeometryCollection))
                    throw new InvalidOperationException();
                return _items ?? (_items = new GeoJsonObjectCollection(this, ToCollectionItemValidator(type)));
            }
        }

        private static Predicate<GeoJsonObject> ToCollectionItemValidator(GeoJsonObjectType type)
        {
            return type == GeoJsonObjectType.FeatureCollection
                 ? (o => o.ObjectType == GeoJsonObjectType.Feature)
                 : (Predicate<GeoJsonObject>) (o => o.IsGeometry);
        }

        public bool HasProperties { get { return _properties != null && _properties.Count > 0; } }

        public JsonObject Properties
        {
            get
            {
                if (ObjectType != GeoJsonObjectType.Feature)
                    throw new InvalidOperationException();
                return _properties ?? (_properties = new JsonObject());
            }
            internal set { _properties = value; }
        }

        public override string ToString()
        {
            StringBuilder sb = new StringBuilder();
            ToJson(JsonText.CreateWriter(sb));
            return sb.ToString();
        }

        public void ToJson(JsonWriter writer)
        {
            ToJson(writer, null);
        }

        public void ToJson(JsonWriter writer, ExportContext context)
        {
            ToJsonImpl(writer, context ?? JsonConvert.CreateExportContext());
        }

        private void ToJsonImpl(JsonWriter writer, ExportContext context)
        {
            if (writer == null) throw new ArgumentNullException("writer");
            if (context == null) throw new ArgumentNullException("context");

            if (SupportsCoordinates && Coordinates == null)
                throw new InvalidOperationException("Coordinates are undefined.");

            context.Register(new GeoJsonPositionExporter());

            writer.WriteStartObject();
            writer.WriteMember("type");
            var type = ObjectType;
            writer.WriteString(type.ToString());
            
            if (IsGeometry)
            {
                if (type == GeoJsonObjectType.GeometryCollection)
                {
                    writer.WriteMember("geometries");
                    WriteCollection(writer, context);
                }
                else
                {
                    writer.WriteMember("coordinates");
                    context.Export(Coordinates, writer);
                }
            }
            else if (type == GeoJsonObjectType.Feature)
            {
                var id = FeatureId;
                if (id != null)
                {
                    writer.WriteMember("id");
                    context.Export(id, writer);
                }

                writer.WriteMember("properties");
                writer.WriteStartObject();
                if (HasProperties)
                {
                    foreach (var property in Properties)
                    {
                        writer.WriteMember(property.Name);
                        context.Export(property.Value, writer);
                    }
                }
                writer.WriteEndObject();

                writer.WriteMember("geometry");
                FeatureGeometry.ToJson(writer, context);
            }
            else if (type == GeoJsonObjectType.FeatureCollection)
            {
                writer.WriteMember("features");
                WriteCollection(writer, context);
            }

            if (HasMembers)
            {
                foreach (var member in Members)
                {
                    writer.WriteMember(member.Name);
                    context.Export(member.Value, writer);
                }
            }

            writer.WriteEndObject();
        }

        private void WriteCollection(JsonWriter writer, ExportContext context)
        {
            Debug.Assert(IsCollection);

            writer.WriteStartArray();
            if (HasItems)
            {
                foreach (var item in Items)
                    item.ToJson(writer, context);
            }
            writer.WriteEndArray();
        }

        public static GeoJsonObject FromJson(string json)
        {
            return FromJson(JsonText.CreateReader(json), null);
        }

        public static GeoJsonObject FromJson(JsonReader reader)
        {
            return FromJson(reader, null);
        }

        public static GeoJsonObject FromJson(JsonReader reader, ImportContext context)
        {
            return FromJsonImpl(reader, context ?? JsonConvert.CreateImportContext());
        }

        private static GeoJsonObject FromJsonImpl(JsonReader reader, ImportContext context)
        {
            if (reader == null) throw new ArgumentNullException("reader");
            if (context == null) throw new ArgumentNullException("context");

            FreeJsonMemberReadingHelper helper = new FreeJsonMemberReadingHelper(reader);
            GeoJsonObjectType type = ParseObjectType(helper.ReadMember("type").ReadString());
            GeoJsonObject obj = new GeoJsonObject(type);
            obj.SelfRead(context, helper);
            return obj;
        }

        private void SelfRead(ImportContext context, FreeJsonMemberReadingHelper helper)
        {
            switch (ObjectType)
            {
                case GeoJsonObjectType.Feature:
                {
                    JsonReader idReader = helper.TryReadMember("id");
                    if (idReader != null)
                        FeatureId = context.Import(idReader);
                    Properties = context.Import<JsonObject>(helper.ReadMember("properties"));
                    FeatureGeometry = FromJson(helper.ReadMember("geometry"), context);
                    break;
                }
                case GeoJsonObjectType.FeatureCollection:
                {
                    ReadCollectionToSelf(helper.ReadMember("features"), context);
                    break;
                }
                case GeoJsonObjectType.GeometryCollection:
                {
                    ReadCollectionToSelf(helper.ReadMember("geometries"), context);
                    break;
                }
                default:
                {
                    Coordinates = GeoJsonCoordsReader.ReadCoords(ObjectType, helper.ReadMember("coordinates"));
                    break;
                }
            }

            ReadMembersToSelf(context, helper.GetTailReader());
        }

        private void ReadMembersToSelf(ImportContext context, JsonReader reader)
        {
            foreach (var item in ReadMembers(context, reader))
                Members.Add(item.Name, item.Value);
        }

        private static IEnumerable<JsonMember> ReadMembers(ImportContext context, JsonReader reader)
        {
            while (reader.TokenClass != JsonTokenClass.EndObject)
                yield return new JsonMember(reader.ReadMember(), context.Import(reader));
            reader.Read();
        }

        private void ReadCollectionToSelf(JsonReader reader, ImportContext context)
        {
            foreach (var item in ReadCollection(reader, context))
                Items.Add(item);
        }

        private static IEnumerable<GeoJsonObject> ReadCollection(JsonReader reader, ImportContext context)
        {
            reader.ReadToken(JsonTokenClass.Array);
            while (reader.TokenClass != JsonTokenClass.EndArray)
                yield return FromJson(reader, context);
            reader.Read();
        }

        private static GeoJsonObjectType ParseObjectType(string type)
        {
            switch (type)
            {
                case "Point"             : return GeoJsonObjectType.Point;                  
                case "MultiPoint"        : return GeoJsonObjectType.MultiPoint;             
                case "LineString"        : return GeoJsonObjectType.LineString;             
                case "MultiLineString"   : return GeoJsonObjectType.MultiLineString;        
                case "Polygon"           : return GeoJsonObjectType.Polygon;                
                case "MultiPolygon"      : return GeoJsonObjectType.MultiPolygon;           
                case "GeometryCollection": return GeoJsonObjectType.GeometryCollection;     
                case "Feature"           : return GeoJsonObjectType.Feature;                
                case "FeatureCollection" : return GeoJsonObjectType.FeatureCollection;      
            }

            throw new GeoJsonException(String.Format("'{0}' is not a valid GeoJSON object type.", type));
        }
    }
}

#endif // !NET_1_0 && !NET_1_1 && !NET_2_0
